@page "/Account/ExternalLogin"

@using System.ComponentModel.DataAnnotations
@using System.Security.Claims
@using System.Text
@using System.Text.Encodings.Web
@using Microsoft.AspNetCore.Identity
@using Microsoft.AspNetCore.Identity.UI.Services
@using Microsoft.AspNetCore.WebUtilities
@using MyApp.Data
@using MyApp.Identity

@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager
@inject IUserStore<ApplicationUser> UserStore
@inject IEmailSender EmailSender
@inject NavigationManager NavigationManager
@inject IdentityRedirectManager RedirectManager
@inject ILogger<ExternalLogin> Logger

@{
    var providerDisplayName = _externalLoginInfo.ProviderDisplayName;
}

<PageTitle>Register</PageTitle>

<div class="mt-8 mx-auto max-w-lg">
    <StatusMessage class="mb-3" Message="@_message" />
    <Heading1>Register</Heading1>
    <Heading2 id="external-login-title">Associate your @providerDisplayName account.</Heading2>

    <Alert id="external-login-description" Type="AlertType.Information" class="my-4">
        You've successfully authenticated with <strong>@providerDisplayName</strong>.
        Please enter an email address for this site below and click the Register button to finish
        logging in.
    </Alert>

    <div class="mt-3 shadow overflow-hidden sm:rounded-md">
        <div class="px-4 bg-white dark:bg-black sm:p-6">
            <EditForm id="confirmation-form" Model="Input" OnValidSubmit="OnValidSubmitAsync" FormName="confirmation" method="post">
                <DataAnnotationsValidator />
                <ValidationSummary class="mb-3 text-danger text-center font-semibold" />

                <div class="flex flex-col gap-4">
                    <div>
                        <label for="email" class="@TextInput.LabelClasses">Email</label>
                        <div class="mt-1 relative rounded-md shadow-sm">
                            <InputText id="email" type="text" @bind-Value="Input.Email" class="@TextInput.InputClasses" autocomplete="email" placeholder="Please enter your email." />
                        </div>
                        <ValidationMessage For="() => Input.Email" class="mt-2 text-danger text-sm" />
                    </div>
                    <div>
                        <PrimaryButton type="submit">Register</PrimaryButton>
                    </div>
                </div>
            </EditForm>
        </div>
    </div>
</div>

@code {
    public const string LoginCallbackAction = "LoginCallback";

    private string? _message;
    private ExternalLoginInfo _externalLoginInfo = default!;
    private IUserEmailStore<ApplicationUser> _emailStore = default!;

    [SupplyParameterFromQuery]
    private string? RemoteError { get; set; }

    [CascadingParameter]
    public HttpContext HttpContext { get; set; } = default!;

    [SupplyParameterFromForm]
    private InputModel Input { get; set; } = default!;

    [SupplyParameterFromQuery]
    private string ReturnUrl { get; set; } = default!;

    [SupplyParameterFromQuery]
    private string? Action { get; set; } = default!;

    protected override async Task OnInitializedAsync()
    {
        Input ??= new();
        ReturnUrl ??= "/";

        if (RemoteError is not null)
        {
            RedirectManager.RedirectToWithStatus("/Account/Login", "Error from external provider: " + RemoteError);
            return;
        }

        var externalLoginInfo = await SignInManager.GetExternalLoginInfoAsync();
        if (externalLoginInfo is null)
        {
            RedirectManager.RedirectToWithStatus("/Account/Login", "Error loading external login information.");
            return;
        }

        _externalLoginInfo = externalLoginInfo;
        _emailStore = GetEmailStore();

        if (HttpMethods.IsGet(HttpContext.Request.Method))
        {
            if (Action == LoginCallbackAction)
            {
                await OnLoginCallbackAsync();
                return;
            }

            // We should only reach this page via the login callback, so redirect back to
            // the login page if we get here some other way.
            RedirectManager.RedirectTo("/Account/Login");
            return;
        }
    }

    private async Task OnLoginCallbackAsync()
    {
        // Sign in the user with this external login provider if the user already has a login.
        var result = await SignInManager.ExternalLoginSignInAsync(
            _externalLoginInfo.LoginProvider,
            _externalLoginInfo.ProviderKey,
            isPersistent: false,
            bypassTwoFactor: true);
        if (result.Succeeded)
        {
            Logger.LogInformation(
                "{Name} logged in with {LoginProvider} provider.",
                _externalLoginInfo.Principal.Identity?.Name,
                _externalLoginInfo.LoginProvider);
            RedirectManager.RedirectTo(ReturnUrl);
            return;
        }

        if (result.IsLockedOut)
        {
            RedirectManager.RedirectTo("/Account/Lockout");
            return;
        }

        // If the user does not have an account, then ask the user to create an account.
        if (_externalLoginInfo.Principal.HasClaim(c => c.Type == ClaimTypes.Email))
        {
            Input.Email = _externalLoginInfo.Principal.FindFirstValue(ClaimTypes.Email);
        }
    }

    private async Task OnValidSubmitAsync()
    {
        var user = CreateUser();

        await UserStore.SetUserNameAsync(user, Input.Email, CancellationToken.None);
        await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None);

        var result = await UserManager.CreateAsync(user);
        if (result.Succeeded)
        {
            result = await UserManager.AddLoginAsync(user, _externalLoginInfo);
            if (result.Succeeded)
            {
                Logger.LogInformation("User created an account using {Name} provider.", _externalLoginInfo.LoginProvider);

                var userId = await UserManager.GetUserIdAsync(user);
                var code = await UserManager.GenerateEmailConfirmationTokenAsync(user);
                code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code));

                var callbackUrl = NavigationManager.GetUriWithQueryParameters(
                    $"{NavigationManager.BaseUri}Account/ConfirmEmail",
                    new Dictionary<string, object?> { { "userId", userId }, { "code", code } });
                await EmailSender.SendEmailAsync(Input.Email!, "Confirm your email",
                    $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>.");

                // If account confirmation is required, we need to show the link if we don't have a real email sender
                if (UserManager.Options.SignIn.RequireConfirmedAccount)
                {
                    RedirectManager.RedirectTo("/Account/RegisterConfirmation", new() { ["Email"] = Input.Email });
                    return;
                }

                await SignInManager.SignInAsync(user, isPersistent: false, _externalLoginInfo.LoginProvider);
                RedirectManager.RedirectTo(ReturnUrl);
                return;
            }
        }
        else
        {
            _message = $"Error: {string.Join(",", result.Errors.Select(error => error.Description))}";
        }
    }

    private ApplicationUser CreateUser()
    {
        try
        {
            return Activator.CreateInstance<ApplicationUser>();
        }
        catch
        {
            throw new InvalidOperationException($"Can't create an instance of '{nameof(ApplicationUser)}'. " +
                $"Ensure that '{nameof(ApplicationUser)}' is not an abstract class and has a parameterless constructor");
        }
    }

    private IUserEmailStore<ApplicationUser> GetEmailStore()
    {
        if (!UserManager.SupportsUserEmail)
        {
            throw new NotSupportedException("The default UI requires a user store with email support.");
        }
        return (IUserEmailStore<ApplicationUser>)UserStore;
    }

    private sealed class InputModel
    {
        [Required]
        [EmailAddress]
        public string? Email { get; set; }
    }
}
